You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ADN-DMR-Peer-Server/tests/routing/test_peer_options_override.py

286 lines
10 KiB

# ADN DMR Peer Server - tests routing peer options override
#
# Copyright (C) 2026 Rodrigo Pérez, CE5RPY <ce5rpy@qmd.cl>
#
###############################################################################
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
###############################################################################
"""Peer RPTO OPTIONS override YAML runtime flags (inject-only proxy)."""
from __future__ import annotations
from tests.harness.deterministic import (
DeterministicScenario,
FakeHbpProtocol,
PacketSpec,
active_routing_table,
)
from adn_server.application.subscription.router import SubscriptionRouter
from adn_server.application.subscription.store_sync import replace_store_from_routing_table
from adn_server.domain import bytes_3, bytes_4
from adn_server.domain.value_objects import TgId
from adn_server.domain.voice_routing import VoiceIngress
def _proxy_system_scenario(
*,
single_mode_yaml: bool = False,
) -> DeterministicScenario:
config = DeterministicScenario().config
sys_cfg = config["SYSTEMS"]["MASTER-A"]
sys_cfg["SINGLE_MODE"] = single_mode_yaml
sys_cfg["DEFAULT_UA_TIMER"] = 60
sys_cfg.pop("OPTIONS", None)
sys_cfg["TS1_STATIC"] = ""
sys_cfg["TS2_STATIC"] = ""
scenario = DeterministicScenario(config=config, routing_table={})
scenario.routing._get_protocols = lambda: scenario.protocols # noqa: SLF001
proto = scenario.protocols["MASTER-A"]
assert isinstance(proto, FakeHbpProtocol)
peer_id = bytes_4(1001)
proto._peers = {
peer_id: {
"CONNECTION": "YES",
"CALLSIGN": b"CE5RPY",
"RADIO_ID": peer_id,
"OPTIONS": b"TS2=730444,52090;SINGLE=1;TIMER=5;",
}
}
return scenario
def test_rpto_single_and_timer_override_yaml() -> None:
scenario = _proxy_system_scenario(single_mode_yaml=False)
scenario.routing.options_config_for_system(
"MASTER-A",
peer_options=b"TS2=730444,52090;SINGLE=1;TIMER=5;",
)
sys_cfg = scenario.config["SYSTEMS"]["MASTER-A"]
assert sys_cfg["SINGLE_MODE"] is True
assert scenario.routing.routing_table_for_report()["730444"][0]["TIMEOUT"] == 300.0
assert scenario.routing.routing_table_for_report()["52090"][0]["TIMEOUT"] == 300.0
def test_options_config_reads_connected_peer_without_yaml_options() -> None:
scenario = _proxy_system_scenario(single_mode_yaml=False)
bridges = active_routing_table(730444, (("MASTER-A", 2), ("MASTER-B", 2)))
scenario.seed_routing_table(bridges)
scenario.routing.options_config_for_system("MASTER-A")
assert scenario.config["SYSTEMS"]["MASTER-A"]["SINGLE_MODE"] is True
def test_single_mode_deactivates_other_static_tg_after_rpto() -> None:
scenario = _proxy_system_scenario(single_mode_yaml=False)
bridges = {
"730444": [
{
"SYSTEM": "MASTER-A",
"TS": 2,
"TGID": bytes_3(730444),
"ACTIVE": True,
"TIMEOUT": 300.0,
"TO_TYPE": "OFF",
"ON": [bytes_3(730444)],
"OFF": [],
"RESET": [],
"TIMER": 0.0,
},
{
"SYSTEM": "MASTER-B",
"TS": 2,
"TGID": bytes_3(730444),
"ACTIVE": True,
"TIMEOUT": 300.0,
"TO_TYPE": "OFF",
"ON": [bytes_3(730444)],
"OFF": [],
"RESET": [],
"TIMER": 0.0,
},
],
"52090": [
{
"SYSTEM": "MASTER-A",
"TS": 2,
"TGID": bytes_3(52090),
"ACTIVE": True,
"TIMEOUT": 300.0,
"TO_TYPE": "OFF",
"ON": [bytes_3(52090)],
"OFF": [],
"RESET": [],
"TIMER": 0.0,
},
{
"SYSTEM": "MASTER-B",
"TS": 2,
"TGID": bytes_3(52090),
"ACTIVE": True,
"TIMEOUT": 300.0,
"TO_TYPE": "OFF",
"ON": [bytes_3(52090)],
"OFF": [],
"RESET": [],
"TIMER": 0.0,
},
],
}
scenario.seed_routing_table(bridges)
scenario.routing.options_config_for_system(
"MASTER-A",
peer_options=b"TS2=730444,52090;SINGLE=1;TIMER=5;",
)
scenario.routing.apply_in_band_signalling("MASTER-A", 2, bytes_3(730444), pkt_time=3000.0)
assert scenario.routing.routing_table_for_report()["730444"][0]["ACTIVE"] is True
assert scenario.routing.routing_table_for_report()["52090"][0]["ACTIVE"] is False
scenario.routing._sync_subscription_store()
legs = SubscriptionRouter(scenario.subscription_store).resolve(
VoiceIngress(
source_system="MASTER-B",
slot=2,
dst_tgid=TgId(52090),
)
)
assert all(leg.target_system != "MASTER-A" for leg in legs)
def test_ua_bridge_creation_pushes_routing_snapshot() -> None:
"""Monitor UA chip needs an immediate routing_table push, not only the 52s rule_timer loop."""
scenario = _proxy_system_scenario()
scenario.config["REPORTS"]["REPORT"] = True
sent: list[tuple[dict, bool]] = []
class _Rec:
def send_routing_table(self, bridges, *, incremental: bool = False) -> None:
sent.append((bridges, incremental))
def send_routing_event(self, _event: str) -> None:
pass
scenario.routing._reporting = _Rec() # noqa: SLF001
base = PacketSpec(dst_id=7304, stream_id=0x22222222, slot=2, peer_id=1001)
scenario.inject_hbp("MASTER-A", DeterministicScenario.voice_head_spec(base))
assert sent
assert sent[-1][1] is True
assert "7304" in sent[-1][0]
def test_ua_bridge_uses_transmitting_peer_timer_minutes() -> None:
"""First TX to unknown TG uses peer OPTIONS TIMER (minutes), not YAML DEFAULT_UA_TIMER."""
scenario = _proxy_system_scenario()
assert scenario.config["SYSTEMS"]["MASTER-A"]["DEFAULT_UA_TIMER"] == 60
base = PacketSpec(dst_id=7304, stream_id=0x11111111, slot=2, peer_id=1001)
scenario.inject_hbp("MASTER-A", DeterministicScenario.voice_head_spec(base))
bridges = scenario.routing.routing_table_for_report()
assert "7304" in bridges
legs = [e for e in bridges["7304"] if e["SYSTEM"] == "MASTER-A" and e["TS"] == 2]
assert len(legs) == 1
assert legs[0]["ACTIVE"] is True
assert legs[0]["TIMEOUT"] == 300.0
def test_peer_timer_does_not_override_other_peer_static_tg_timeout() -> None:
"""Each hotspot TIMER applies only to that peer's static TGs (no max() across peers)."""
config = DeterministicScenario().config
config["SYSTEMS"]["MASTER-A"]["DEFAULT_UA_TIMER"] = 60
scenario = DeterministicScenario(config=config, routing_table={})
scenario.routing._get_protocols = lambda: scenario.protocols # noqa: SLF001
proto = scenario.protocols["MASTER-A"]
assert isinstance(proto, FakeHbpProtocol)
proto._peers = {
bytes_4(1001): {
"CONNECTION": "YES",
"CALLSIGN": b"CA1ROG",
"RADIO_ID": bytes_4(1001),
"OPTIONS": b'TS2=730444;TIMER=300;',
},
bytes_4(1002): {
"CONNECTION": "YES",
"CALLSIGN": b"CE5RPY",
"RADIO_ID": bytes_4(1002),
"OPTIONS": b"TS2=214091;SINGLE=1;TIMER=5;",
},
}
scenario.routing.options_config_for_system("MASTER-A")
bridges = scenario.routing.routing_table_for_report()
assert bridges["730444"][0]["TIMEOUT"] == 300.0 * 60.0
assert bridges["214091"][0]["TIMEOUT"] == 5.0 * 60.0
scenario.routing.options_config_for_system(
"MASTER-A",
peer_options=b"TS2=214091;SINGLE=1;TIMER=5;",
)
bridges = scenario.routing.routing_table_for_report()
assert bridges["730444"][0]["TIMEOUT"] == 300.0 * 60.0
assert bridges["214091"][0]["TIMEOUT"] == 5.0 * 60.0
def test_make_static_tg_rearms_static_legs_like_legacy() -> None:
"""Legacy make_static_tg always sets ACTIVE True (TO_TYPE OFF); SINGLE contention is in-band only."""
scenario = _proxy_system_scenario(single_mode_yaml=True)
bridges = {
"730444": [
{
"SYSTEM": "MASTER-A",
"TS": 2,
"TGID": bytes_3(730444),
"ACTIVE": False,
"TIMEOUT": 300.0,
"TO_TYPE": "OFF",
"ON": [bytes_3(730444)],
"OFF": [],
"RESET": [],
"TIMER": 9_999_999.0,
}
],
"7305": [
{
"SYSTEM": "MASTER-A",
"TS": 2,
"TGID": bytes_3(7305),
"ACTIVE": True,
"TIMEOUT": 300.0,
"TO_TYPE": "OFF",
"ON": [bytes_3(7305)],
"OFF": [],
"RESET": [],
"TIMER": 9_999_998.0,
}
],
}
scenario.seed_routing_table(bridges)
replace_store_from_routing_table(scenario.subscription_store, bridges)
scenario.routing.make_static_tg(730444, 2, 5.0, "MASTER-A")
scenario.routing.make_static_tg(7305, 2, 5.0, "MASTER-A")
assert scenario.routing.routing_table_for_report()["730444"][0]["ACTIVE"] is True
assert scenario.routing.routing_table_for_report()["7305"][0]["ACTIVE"] is True

Powered by TurnKey Linux.