| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- """线架注册模型(地址段扫描 / UID 绑定 / 换架自动接管)单元测试。"""
- import sys
- import os
- from unittest.mock import patch
- import pytest
- sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
- import app as app_module # noqa: E402
- from app import app # noqa: E402
- def make_cards(port_map):
- """构造 24 路读卡结果;port_map: {port_id: 'hex12'}"""
- cards = []
- for ant in range(1, 25):
- hex_str = (port_map.get(ant) or '').lower()
- cards.append({
- 'antenna': ant,
- 'card_str': hex_str,
- 'present': bool(hex_str),
- 'uid': '' if not hex_str else ':'.join(hex_str[i:i + 2] for i in range(0, 12, 2)),
- })
- return cards
- def read_result(cards, panel_uid=None):
- return {
- 'success': True, 'device_address': 1, 'function_code': 3,
- 'cards': cards, 'panel_uid': panel_uid, 'raw_data': 'TEST',
- }
- class TestPanelRegistration:
- def setup_method(self):
- self._saved = {
- 'stored': dict(app_module.address_config.stored_devices),
- 'panel_config': dict(app_module.panel_config),
- 'port_state': dict(app_module.port_state),
- 'fail': dict(app_module.panel_fail_count),
- 'address_range': app_module.dtu_config.get('address_range'),
- 'port_mappings': app_module.dtu_config.get('port_mappings'),
- 'last_seen': dict(app_module.device_last_seen),
- }
- self.mock_modbus = patch.object(app_module, 'modbus_client').start()
- self.mock_save_dev = patch.object(app_module, 'save_device_config').start()
- self.mock_save_dtu = patch.object(app_module, 'save_dtu_config').start()
- self.mock_register = patch.object(app_module, 'dtu_register').start()
- self.mock_panel_status = patch.object(app_module, 'dtu_publish_panel_status').start()
- app_module.address_config.stored_devices.clear()
- app_module.panel_config.clear()
- app_module.port_state.clear()
- app_module.panel_fail_count.clear()
- app_module.device_last_seen.clear()
- def teardown_method(self):
- patch.stopall()
- app_module.address_config.stored_devices.clear()
- app_module.address_config.stored_devices.update(self._saved['stored'])
- app_module.panel_config.clear()
- app_module.panel_config.update(self._saved['panel_config'])
- app_module.port_state.clear()
- app_module.port_state.update(self._saved['port_state'])
- app_module.panel_fail_count.clear()
- app_module.panel_fail_count.update(self._saved['fail'])
- app_module.device_last_seen.clear()
- app_module.device_last_seen.update(self._saved['last_seen'])
- if self._saved['address_range'] is None:
- app_module.dtu_config.pop('address_range', None)
- else:
- app_module.dtu_config['address_range'] = self._saved['address_range']
- if self._saved['port_mappings'] is None:
- app_module.dtu_config.pop('port_mappings', None)
- else:
- app_module.dtu_config['port_mappings'] = self._saved['port_mappings']
- def test_takeover_rebinds_uid_and_inherits_mappings(self):
- """换架: 原地址探测到新 UID -> 重绑地址、保留 panel_id/映射、自动下发卡号。"""
- old_uid = 'a1' * 12
- new_uid = 'b2' * 12
- pid = 'PANEL_dtu_001_5'
- app_module.dtu_config['address_range'] = {'start': 5, 'end': 5}
- app_module.dtu_config['port_mappings'] = {pid: {'3': 'AABBCCDDEEFF'}}
- app_module.address_config.stored_devices[old_uid] = 5
- app_module.panel_config[pid] = {'address': 5, 'position': 5, 'panel_uid': old_uid, 'status': 'offline'}
- app_module.panel_fail_count[pid] = 3
- self.mock_modbus.read_all_antenna_cards.return_value = read_result(
- make_cards({3: 'aabbccddeeff'}), panel_uid=new_uid)
- self.mock_modbus.set_card_mapping.return_value = {'success': True}
- app_module._sweep_panel_addresses()
- devices = app_module.address_config.stored_devices
- assert devices.get(new_uid) == 5
- assert old_uid not in devices
- assert app_module.panel_config[pid]['panel_uid'] == new_uid
- assert app_module.panel_config[pid]['status'] == 'online'
- assert pid not in app_module.panel_fail_count
- # 期望卡号已下发到硬件(继承原面板数据)
- args, _ = self.mock_modbus.set_card_mapping.call_args
- assert args[0] == 5
- assert args[1].get(3) == 'AABBCCDDEEFF'
- assert self.mock_save_dev.called
- assert self.mock_register.called
- def test_probe_same_uid_marks_back_online(self):
- """缺失探测回应原 UID -> 恢复在线,不改变绑定。"""
- uid = 'c3' * 12
- pid = 'PANEL_dtu_001_2'
- app_module.dtu_config['address_range'] = {'start': 2, 'end': 2}
- app_module.address_config.stored_devices[uid] = 2
- app_module.panel_config[pid] = {'address': 2, 'position': 2, 'panel_uid': uid, 'status': 'offline'}
- app_module.panel_fail_count[pid] = 3
- self.mock_modbus.read_all_antenna_cards.return_value = read_result(
- make_cards({}), panel_uid=uid)
- app_module._sweep_panel_addresses()
- assert app_module.address_config.stored_devices == {uid: 2}
- assert app_module.panel_config[pid]['status'] == 'online'
- assert pid not in app_module.panel_fail_count
- def test_unbound_probe_registers_new_panel(self):
- """未绑定地址探测到新线架 -> 绑定地址↔UID。"""
- new_uid = 'd4' * 12
- app_module.dtu_config['address_range'] = {'start': 4, 'end': 4}
- self.mock_modbus.read_all_antenna_cards.return_value = read_result(
- make_cards({}), panel_uid=new_uid)
- app_module._sweep_panel_addresses()
- assert app_module.address_config.stored_devices.get(new_uid) == 4
- assert 'PANEL_dtu_001_4' in app_module.panel_config
- assert self.mock_save_dev.called
- def test_missing_panel_suspends_unbound_probe(self):
- """有面板处于缺失/接管状态时,本轮不抽查未绑定地址。"""
- old_uid = 'e5' * 12
- pid = 'PANEL_dtu_001_2'
- app_module.dtu_config['address_range'] = {'start': 2, 'end': 3}
- app_module.address_config.stored_devices[old_uid] = 2
- app_module.panel_config[pid] = {'address': 2, 'position': 2, 'panel_uid': old_uid, 'status': 'offline'}
- app_module.panel_fail_count[pid] = 1
- self.mock_modbus.read_all_antenna_cards.return_value = {'error': 'timeout'}
- app_module._sweep_panel_addresses()
- probed = [c.args[0] for c in self.mock_modbus.read_all_antenna_cards.call_args_list]
- assert probed == [2]
- def test_read_failure_threshold_marks_offline(self):
- """连续失败达到阈值 -> 标记离线(并发布离线状态)。"""
- uid = 'f6' * 12
- pid = 'PANEL_dtu_001_3'
- app_module.dtu_config['address_range'] = {'start': 3, 'end': 3}
- app_module.address_config.stored_devices[uid] = 3
- app_module.panel_config[pid] = {'address': 3, 'position': 3, 'panel_uid': uid, 'status': 'online'}
- self.mock_modbus.read_all_antenna_cards.return_value = {'error': 'timeout'}
- for _ in range(app_module.PANEL_MISS_THRESHOLD):
- app_module._sweep_panel_addresses()
- assert app_module.panel_config[pid]['status'] == 'offline'
- assert app_module.panel_fail_count[pid] == app_module.PANEL_MISS_THRESHOLD
- self.mock_panel_status.assert_any_call(pid, 3, None, online=False)
- def test_panel_unbind_api(self):
- """解绑 API: 移除绑定与面板配置。"""
- client = app.test_client()
- uid = '99' * 12
- pid = 'PANEL_dtu_001_7'
- app_module.address_config.stored_devices[uid] = 7
- app_module.panel_config[pid] = {'address': 7, 'position': 7, 'panel_uid': uid, 'status': 'online'}
- resp = client.post('/api/panel/unbind', json={'address': 7})
- data = resp.get_json()
- assert resp.status_code == 200 and data['success']
- assert uid not in app_module.address_config.stored_devices
- assert pid not in app_module.panel_config
- assert self.mock_save_dev.called
- def test_update_dtu_config_address_range(self):
- """DTU 配置 API: 地址段写入并校验。"""
- client = app.test_client()
- resp = client.post('/api/dtu/config', json={'address_range': {'start': 5, 'end': 60}})
- assert resp.status_code == 200
- assert app_module.dtu_config['address_range'] == {'start': 5, 'end': 60}
- assert self.mock_save_dtu.called
- resp = client.post('/api/dtu/config', json={'address_range': {'start': 70, 'end': 10}})
- assert resp.status_code == 400
- assert app_module.dtu_config['address_range'] == {'start': 5, 'end': 60}
- def test_address_range_fallback(self):
- app_module.dtu_config['address_range'] = {'start': 60, 'end': 10}
- assert app_module._address_range() == (1, 50)
- app_module.dtu_config['address_range'] = {'start': 3, 'end': 9}
- assert app_module._address_range() == (3, 9)
- app_module.dtu_config['address_range'] = None
- assert app_module._address_range() == (1, 50)
- if __name__ == '__main__':
- pytest.main([__file__, '-v'])
|