"""In-process deterministic packet harness for bridge_master tests. This module is test-only. It avoids UDP sockets and replaces production network sends with capture functions while leaving production modules unchanged. """ from __future__ import annotations from dataclasses import dataclass, field from types import SimpleNamespace import copy import importlib import unittest DMRD = b"DMRD" HBPF_VOICE = 0x0 HBPF_VOICE_SYNC = 0x1 HBPF_DATA_SYNC = 0x2 HBPF_SLT_VHEAD = 0x1 HBPF_SLT_VTERM = 0x2 ID_MAX = 16776415 PEER_MAX = 4294967295 def require_bridge_master(): """Import bridge_master or skip tests when runtime deps are unavailable.""" try: return importlib.import_module("bridge_master") except ModuleNotFoundError as exc: raise unittest.SkipTest( f"bridge_master runtime dependency is not installed: {exc.name}" ) from exc def bytes_3(value: int | bytes) -> bytes: if isinstance(value, bytes): if len(value) != 3: raise ValueError("expected exactly 3 bytes") return value return int(value).to_bytes(3, "big") def bytes_4(value: int | bytes) -> bytes: if isinstance(value, bytes): if len(value) != 4: raise ValueError("expected exactly 4 bytes") return value return int(value).to_bytes(4, "big") def int_id(value: int | bytes) -> int: if isinstance(value, int): return value return int.from_bytes(value, "big") def acl_permit_all(max_id: int = ID_MAX) -> tuple[bool, list[tuple[int, int]]]: return True, [(1, max_id)] def hbp_bits(slot: int, call_type: str, frame_type: int, dtype_vseq: int) -> int: bits = ((frame_type & 0x3) << 4) | (dtype_vseq & 0xF) if slot == 2: bits |= 0x80 if call_type == "unit": bits |= 0x40 return bits def parse_dmr_fields(packet: bytes) -> dict[str, object]: if len(packet) < 20 or packet[:4] != DMRD: return {"raw": packet} bits = packet[15] if bits & 0x40: call_type = "unit" elif (bits & 0x23) == 0x23: call_type = "vcsbk" else: call_type = "group" return { "opcode": packet[:4], "seq": packet[4], "rf_src": packet[5:8], "dst_id": packet[8:11], "peer_id": packet[11:15], "bits": bits, "slot": 2 if bits & 0x80 else 1, "call_type": call_type, "frame_type": (bits & 0x30) >> 4, "dtype_vseq": bits & 0xF, "stream_id": packet[16:20], "dmr_payload": packet[20:53], "ber": packet[53:54], "rssi": packet[54:55], } @dataclass(frozen=True) class PacketSpec: peer_id: int | bytes = 1001 rf_src: int | bytes = 3120001 dst_id: int | bytes = 91 slot: int = 2 stream_id: int | bytes = 0x01020304 seq: int = 0 call_type: str = "group" frame_type: int = HBPF_VOICE dtype_vseq: int = 0 payload: bytes = b"\x00" * 33 ber: bytes = b"\x00" rssi: bytes = b"\x00" delay: float = 0.0 def data(self) -> bytes: if len(self.payload) != 33: raise ValueError("DMR payload must be exactly 33 bytes") return b"".join( [ DMRD, bytes([self.seq & 0xFF]), bytes_3(self.rf_src), bytes_3(self.dst_id), bytes_4(self.peer_id), bytes([hbp_bits(self.slot, self.call_type, self.frame_type, self.dtype_vseq)]), bytes_4(self.stream_id), self.payload, self.ber, self.rssi, ] ) def decoded_args(self) -> tuple[bytes, bytes, bytes, int, int, str, int, int, bytes, bytes]: return ( bytes_4(self.peer_id), bytes_3(self.rf_src), bytes_3(self.dst_id), self.seq & 0xFF, self.slot, self.call_type, self.frame_type, self.dtype_vseq, bytes_4(self.stream_id), self.data(), ) def decoded_obp_args( self, packet_hash: bytes = b"", hops: bytes = b"", source_server: int | bytes = 9990, source_rptr: int | bytes = 0, ) -> tuple[bytes, bytes, bytes, int, int, str, int, int, bytes, bytes, bytes, bytes, bytes, bytes, bytes, bytes]: return ( bytes_4(self.peer_id), bytes_3(self.rf_src), bytes_3(self.dst_id), self.seq & 0xFF, self.slot, self.call_type, self.frame_type, self.dtype_vseq, bytes_4(self.stream_id), self.data(), packet_hash, hops, bytes_4(source_server), self.ber, self.rssi, bytes_4(source_rptr), ) @dataclass class CapturedPacket: target_system: str packet: bytes hops: bytes | None = None ber: bytes = b"\x00" rssi: bytes = b"\x00" source_server: bytes = b"\x00\x00\x00\x00" source_rptr: bytes = b"\x00\x00\x00\x00" fields: dict[str, object] = field(init=False) def __post_init__(self) -> None: self.fields = parse_dmr_fields(self.packet) class PacketCapture: def __init__(self) -> None: self.packets: list[CapturedPacket] = [] def recorder(self, target_system: str): def record( packet: bytes, hops: bytes | None = b"", ber: bytes = b"\x00", rssi: bytes = b"\x00", source_server: bytes = b"\x00\x00\x00\x00", source_rptr: bytes = b"\x00\x00\x00\x00", ) -> None: self.packets.append( CapturedPacket( target_system=target_system, packet=packet, hops=hops, ber=ber, rssi=rssi, source_server=source_server, source_rptr=source_rptr, ) ) return record def for_system(self, system: str) -> list[CapturedPacket]: return [packet for packet in self.packets if packet.target_system == system] class ReportCapture: def __init__(self) -> None: self.events: list[bytes] = [] def send_bridgeEvent(self, data: bytes) -> None: self.events.append(data) class FakeClock: def __init__(self, start: float = 1_700_000_000.0) -> None: self.now = float(start) def time(self) -> float: return self.now def advance(self, seconds: float) -> float: self.now += seconds return self.now class FakeReactor: def __init__(self) -> None: self.later: list[tuple[float, object, tuple, dict]] = [] self.thread_calls: list[tuple[object, tuple, dict]] = [] def callLater(self, delay, func, *args, **kwargs): self.later.append((delay, func, args, kwargs)) return SimpleNamespace(cancel=lambda: None, active=lambda: True) def callInThread(self, func, *args, **kwargs): self.thread_calls.append((func, args, kwargs)) def callFromThread(self, func, *args, **kwargs): return func(*args, **kwargs) class FakeTransport: def __init__(self) -> None: self.writes: list[tuple[bytes, tuple[str, int] | None]] = [] def write(self, packet: bytes, sockaddr=None) -> None: self.writes.append((packet, sockaddr)) def minimal_config(system_names: tuple[str, ...] = ("MASTER-A", "MASTER-B")) -> dict: config = { "GLOBAL": { "SERVER_ID": bytes_4(9990), "USE_ACL": False, "TG1_ACL": acl_permit_all(), "TG2_ACL": acl_permit_all(), "SUB_ACL": acl_permit_all(), "GEN_STAT_BRIDGES": False, "DATA_GATEWAY": False, "VALIDATE_SERVER_IDS": False, }, "REPORTS": {"REPORT": False}, "ALIASES": {"PATH": "./", "SUB_MAP_FILE": ""}, "ALLSTAR": {"ENABLED": False}, "SYSTEMS": {}, "_SUB_IDS": {}, "_PEER_IDS": {}, "_LOCAL_SUBSCRIBER_IDS": {}, "_SERVER_IDS": {}, "CHECKSUMS": {}, } for name in system_names: config["SYSTEMS"][name] = { "MODE": "MASTER", "ENABLED": True, "REPEAT": True, "MAX_PEERS": 1, "IP": "127.0.0.1", "PORT": 0, "PASSPHRASE": b"", "GROUP_HANGTIME": 0, "USE_ACL": False, "REG_ACL": acl_permit_all(PEER_MAX), "SUB_ACL": acl_permit_all(), "TG1_ACL": acl_permit_all(), "TG2_ACL": acl_permit_all(), "DEFAULT_UA_TIMER": 1, "SINGLE_MODE": True, "VOICE_IDENT": False, "DIAL_A_TG": True, "DYNAMIC_TG_ROUTING": True, "TS1_STATIC": "", "TS2_STATIC": "", "DEFAULT_DIAL_TS1": 0, "DEFAULT_DIAL_TS2": 0, "DEFAULT_REFLECTOR": 0, "GENERATOR": 0, "ANNOUNCEMENT_LANGUAGE": "en_GB", "ALLOW_UNREG_ID": True, "PROXY_CONTROL": False, "OVERRIDE_IDENT_TG": False, "PEERS": {}, } return config def add_openbridge_system(config: dict, name: str = "OBP-1", network_id: int = 1) -> dict: config["SYSTEMS"][name] = { "MODE": "OPENBRIDGE", "ENABLED": True, "NETWORK_ID": bytes_4(network_id), "IP": "127.0.0.1", "PORT": 0, "PASSPHRASE": b"test-passphrase\x00\x00\x00\x00\x00\x00", "TARGET_IP": "127.0.0.1", "TARGET_PORT": 0, "TARGET_SOCK": ("127.0.0.1", 0), "USE_ACL": False, "SUB_ACL": acl_permit_all(), "TG1_ACL": acl_permit_all(), "TG2_ACL": acl_permit_all(), "RELAX_CHECKS": True, "ENHANCED_OBP": False, "VER": 5, } return config def active_bridge( name: str, tg_id: int, entries: tuple[tuple[str, int], ...], timeout_minutes: int = 1, ) -> dict[str, list[dict]]: tg_bytes = bytes_3(tg_id) return { name: [ { "SYSTEM": system, "TS": slot, "TGID": tg_bytes, "ACTIVE": True, "TIMEOUT": timeout_minutes * 60, "TO_TYPE": "ON", "OFF": [], "ON": [tg_bytes], "RESET": [], "TIMER": 0, } for system, slot in entries ] } class DeterministicScenario: def __init__(self, config: dict | None = None, bridges: dict | None = None) -> None: self.config = config or minimal_config() self.bridges = bridges or {} self.clock = FakeClock() self.capture = PacketCapture() self.reports: dict[str, ReportCapture] = {} self.transports: dict[str, FakeTransport] = {} self.reactor = FakeReactor() self.bm = None self._saved_attrs: dict[str, object] = {} self._saved_systems: dict | None = None def __enter__(self): self.bm = require_bridge_master() self._saved_systems = dict(self.bm.systems) for attr in ( "CONFIG", "BRIDGES", "SUB_MAP", "peer_ids", "subscriber_ids", "talkgroup_ids", "local_subscriber_ids", "server_ids", "checksums", "reactor", "time", "words", ): if hasattr(self.bm, attr): self._saved_attrs[attr] = getattr(self.bm, attr) self.bm.CONFIG = self.config self.bm.BRIDGES = copy.deepcopy(self.bridges) self.bm.SUB_MAP = {} self.bm.peer_ids = {} self.bm.subscriber_ids = {} self.bm.talkgroup_ids = {} self.bm.local_subscriber_ids = {} self.bm.server_ids = {} self.bm.checksums = {} self.bm.words = {"en_GB": {"silence": b"", "busy": b"", "notlinked": b"", "linkedto": b"", "to": b""}} self.bm.reactor = self.reactor self.bm.time = self.clock.time self.bm.systems.clear() for system_name, system_config in self.config["SYSTEMS"].items(): report = ReportCapture() self.reports[system_name] = report if system_config["MODE"] == "MASTER": system = self.bm.routerHBP(system_name, self.config, report) elif system_config["MODE"] == "OPENBRIDGE": system = self.bm.routerOBP(system_name, self.config, report) else: continue system.send_system = self.capture.recorder(system_name) transport = FakeTransport() system.transport = transport self.transports[system_name] = transport self.bm.systems[system_name] = system return self def __exit__(self, exc_type, exc, tb) -> None: if self.bm is None: return self.bm.systems.clear() if self._saved_systems is not None: self.bm.systems.update(self._saved_systems) for attr in ( "CONFIG", "BRIDGES", "SUB_MAP", "peer_ids", "subscriber_ids", "talkgroup_ids", "local_subscriber_ids", "server_ids", "checksums", "reactor", "time", "words", ): if attr in self._saved_attrs: setattr(self.bm, attr, self._saved_attrs[attr]) elif hasattr(self.bm, attr): delattr(self.bm, attr) @property def systems(self): return self.bm.systems @property def bridge_state(self): return self.bm.BRIDGES def inject_hbp(self, system_name: str, packet: PacketSpec) -> None: self.systems[system_name].dmrd_received(*packet.decoded_args()) def inject_obp(self, system_name: str, packet: PacketSpec) -> None: self.systems[system_name].dmrd_received(*packet.decoded_obp_args()) def inject_datagram(self, system_name: str, packet: bytes, sockaddr=("127.0.0.1", 50000)) -> None: self.systems[system_name].datagramReceived(packet, sockaddr) def register_peer( self, system_name: str, peer_id: int | bytes = 1001, sockaddr=("127.0.0.1", 50000), callsign: bytes = b"TEST ", ) -> bytes: peer = bytes_4(peer_id) self.config["SYSTEMS"][system_name]["PEERS"][peer] = { "CONNECTION": "YES", "SOCKADDR": sockaddr, "CALLSIGN": callsign, "RADIO_ID": peer, "LAST_PING": self.clock.time(), } return peer