Files
Cort BuffingtonandClaude Opus 4.8 313cd1de8e tests: migrate harness off the dropped MASTER/PEER terminology
The test harness still used the old config MODE vocabulary (MASTER/PEER) that the
current schema no longer accepts, so build_config() silently dropped those
systems and ~33 tests failed (config not loaded). Migrate the fixtures and test
code to the current terms; the whole suite (52 tests) now passes.

- harness.cfg: MODE MASTER->SERVER, PEER->OUTBOUND; MAX_PEERS->MAX_REPEATERS;
  MASTER_IP/PORT->SERVER_IP/PORT. System labels MASTER-1/-2 -> SERVER-1/-2
  (REPEATER-1 kept; 'repeater' is not a dropped term).
- test code: _peers->_repeaters, send_master->send_server,
  MASTER_SOCKADDR->SERVER_SOCKADDR, send_bridgeEvent->send_bridge_event mocks.
- harness.py: the 'mark connected' gate keyed on mode=='PEER' -> 'OUTBOUND', so
  the router again forwards to outbound systems (fixes the routing/baseline
  scenarios).
- Comments/identifiers describing our system roles moved off master/peer;
  genuine HBP wire-protocol terms (peer-id, the DMRD peer field, the homebrew
  login handshake) are left as-is.
- baseline_emissions.json regenerated consistently for the renamed systems.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 18:07:00 -05:00

178 lines
7.0 KiB
Python

#!/usr/bin/env python
#
# Frame-replay test harness for the bridge routing core.
#
# It stands up the REAL routerHBP / routerOBP instances against a small config,
# drives synthetic DMRD frames through their routing entry point (dmrd_received),
# and captures the packets each system would emit (send_system). A controllable
# clock makes contention/hangtime deterministic.
#
# Purpose: characterize the current routing behavior and, during the planned
# unification, run identical frame sequences through the old and new code and
# assert byte-identical emissions -- so OBP multi-stream behavior and HBP slot
# locking can be proven preserved without a live over-the-air network.
#
# This module is scaffolding, not a test itself; see test_harness_smoke.py.
import os
import sys
import logging
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import bridge
import config
# Keep the routers quiet during replay (they log heavily at INFO). Raise only
# their own logger levels rather than logging.disable() -- a global disable would
# also defeat assertLogs() checks in other test modules. setLevel is fine because
# assertLogs temporarily lowers the level within its own context.
for _name in ('bridge', 'hblink'):
logging.getLogger(_name).setLevel(logging.CRITICAL)
# Self-contained test topology: two HBP masters, one HBP peer, two OpenBridges.
TEST_CFG = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'harness.cfg')
# Bit-field constants for assembling the DMRD flags byte (see datagramReceived).
_FT_VOICE = 0 # HBPF_VOICE
_FT_DATA_SYNC = 2 # HBPF_DATA_SYNC
_VHEAD = 1 # HBPF_SLT_VHEAD
_VTERM = 2 # HBPF_SLT_VTERM
class Clock:
"""A manually advanced stand-in for time.time()."""
def __init__(self, start=1_000_000.0):
self.t = start
def __call__(self):
return self.t
def tick(self, seconds):
self.t += seconds
return self.t
def mk_dmrd(seq, rf_src, dst, peer, slot, call_type, frame_type, dtype_vseq,
stream_id, dmrpkt=None):
"""Assemble a 55-byte DMRD packet (33-byte payload + 2 BER/RSSI bytes)."""
if dmrpkt is None:
dmrpkt = bytes((i * 5 + 1) & 0xFF for i in range(33))
assert len(rf_src) == 3 and len(dst) == 3 and len(peer) == 4 and len(stream_id) == 4
assert len(dmrpkt) == 33
bits = 0
if slot == 2:
bits |= 0x80
if call_type == 'unit':
bits |= 0x40
bits |= (frame_type & 0x3) << 4
bits |= (dtype_vseq & 0xF)
return b''.join([b'DMRD', bytes([seq]), rf_src, dst, peer,
bytes([bits]), stream_id, dmrpkt, b'\x00\x00'])
class World:
"""A small routing world: a set of systems joined to bridges, with a clock
and an emission-capture log."""
def __init__(self, bridges, unit_systems=None, extra_systems=None):
# Fresh, isolated module state for the bridge module.
self.clock = Clock()
bridge.time = self.clock # monkeypatch the clock the routers read
self.CONFIG = config.build_config(TEST_CFG)
self.CONFIG['REPORTS']['REPORT'] = False
bridge.CONFIG = self.CONFIG
# Aliases used only for logging; empty is fine (get_alias returns the id).
bridge.subscriber_ids = {}
bridge.peer_ids = {}
bridge.talkgroup_ids = {}
bridge.UNIT_MAP = {}
bridge.UNIT = list(unit_systems or [])
# Build the BRIDGES structure and indexes from the supplied spec.
bridge.BRIDGES = bridge.make_bridges(bridges)
bridge.BRIDGE_SRC_INDEX, bridge.BRIDGE_BY_SYSTEM = bridge.index_bridges(bridge.BRIDGES)
# Instantiate the real router objects for every system referenced.
self.captures = []
bridge.systems.clear()
names = set(unit_systems or []) | set(extra_systems or [])
for b in bridges:
for member in bridges[b]:
names.add(member['SYSTEM'])
for name in names:
mode = self.CONFIG['SYSTEMS'][name]['MODE']
if mode == 'OPENBRIDGE':
obj = bridge.routerOBP(name, self.CONFIG, None)
else:
obj = bridge.routerHBP(name, self.CONFIG, None)
# The router only forwards to an OUTBOUND system once it has
# logged into its upstream server; the harness models connected,
# participating systems, so mark outbound systems as connected.
if mode == 'OUTBOUND':
obj._stats['CONNECTION'] = 'YES'
# Capture what the routing core hands to each system to transmit,
# instead of writing to a real UDP transport.
obj.send_system = self._make_capture(name)
bridge.systems[name] = obj
# The STATUS tables were initialized with the current clock value. Advance
# well past any hangtime/stream window so the first real frame isn't seen
# as colliding with construction-time state (matches "started, then idle").
self.clock.tick(3600.0)
def _make_capture(self, name):
def _capture(_packet):
self.captures.append((name, bytes(_packet)))
return _capture
def feed(self, src, *, rf_src, dst, peer, slot, stream_id, call_type,
frame_type, dtype_vseq, seq=0):
"""Drive one DMRD frame into src's routing entry point."""
data = mk_dmrd(seq, rf_src, dst, peer, slot, call_type,
frame_type, dtype_vseq, stream_id)
bridge.systems[src].dmrd_received(
peer, rf_src, dst, seq, slot, call_type,
frame_type, dtype_vseq, stream_id, data)
def feed_group_header(self, src, **kw):
self.feed(src, call_type='group', frame_type=_FT_DATA_SYNC,
dtype_vseq=_VHEAD, **kw)
def feed_group_terminator(self, src, **kw):
self.feed(src, call_type='group', frame_type=_FT_DATA_SYNC,
dtype_vseq=_VTERM, **kw)
def feed_group_burst(self, src, burst, **kw):
self.feed(src, call_type='group', frame_type=_FT_VOICE,
dtype_vseq=burst, **kw)
def feed_unit_header(self, src, **kw):
self.feed(src, call_type='unit', frame_type=_FT_DATA_SYNC,
dtype_vseq=_VHEAD, **kw)
def feed_unit_terminator(self, src, **kw):
self.feed(src, call_type='unit', frame_type=_FT_DATA_SYNC,
dtype_vseq=_VTERM, **kw)
def feed_unit_burst(self, src, burst, **kw):
self.feed(src, call_type='unit', frame_type=_FT_VOICE,
dtype_vseq=burst, **kw)
def seed_unit_map(self, subscriber, system):
"""Register that `subscriber` (3 bytes) was last heard on `system`, so a
unit call to it routes there."""
bridge.UNIT_MAP[subscriber] = (system, self.clock())
def drain(self):
"""Return and clear captured emissions as a list of (system, bytes)."""
out = self.captures[:]
self.captures.clear()
return out
def emitted_to(self, system):
return [pkt for (name, pkt) in self.captures if name == system]