mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-11-01 10:32:23 +00:00
First slice of the maintainability refactor: golden-file characterization of behavior that later commits consolidate. The scope matrix runs every resolver (MeshMessage, resolve/effective_channel_send_scope, channel_body_budget, the scheduler's budget resolver, BaseServicePlugin.get_mesh_flood_scope, and the scopes a real send_channel_message hands to set_flood_scope) against 420 combinations of override, per-channel and section scope. The identity matrix runs each node-name lookup against every self_info shape. Both record today's divergences rather than fixing them, so a refactor commit that changes either golden file is a behavior change by definition.
164 lines
5.9 KiB
Python
164 lines
5.9 KiB
Python
"""Characterization: every flood-scope resolver against every scope source.
|
|
|
|
Pins today's behavior, divergences included (the scheduler budgets ``*`` as
|
|
regional, and an explicitly empty per-channel scope falls through to the
|
|
override in its resolver). A refactor that consolidates scope resolution must
|
|
leave this golden file unchanged; a deliberate fix regenerates it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import configparser
|
|
import itertools
|
|
from unittest.mock import AsyncMock, MagicMock, Mock
|
|
|
|
from meshcore import EventType
|
|
|
|
from modules.command_manager import CommandManager
|
|
from modules.models import MeshMessage
|
|
from modules.scheduler import MessageScheduler
|
|
from modules.service_plugins.base_service import BaseServicePlugin
|
|
from tests.characterization.golden_util import assert_golden
|
|
|
|
UNSET = "<unset>"
|
|
OVERRIDES = [UNSET, "", "#west", "west", "*", "none", " #pad "]
|
|
CHANNEL_SCOPES = [UNSET, "", "*", "#sea", "0"]
|
|
SECTION_SCOPES = [UNSET, "", "#svc", "*"]
|
|
EXPLICIT = [None, "", "*", "west", "#east", "None"]
|
|
REPLY = [None, "", "#r", "*"]
|
|
|
|
|
|
class _Svc(BaseServicePlugin):
|
|
config_section = "Svc"
|
|
|
|
async def start(self): # pragma: no cover - abstract stub
|
|
pass
|
|
|
|
async def stop(self): # pragma: no cover - abstract stub
|
|
pass
|
|
|
|
|
|
def _config(override, channel_scope, section_scope):
|
|
cfg = configparser.ConfigParser()
|
|
cfg.add_section("Bot")
|
|
cfg.set("Bot", "bot_name", "ScopeBot")
|
|
cfg.add_section("Channels")
|
|
cfg.set("Channels", "monitor_channels", "general")
|
|
if override is not UNSET:
|
|
cfg.set("Channels", "outgoing_flood_scope_override", override)
|
|
if channel_scope is not UNSET:
|
|
cfg.set("Channels", "flood_scope.general", channel_scope)
|
|
cfg.add_section("Svc")
|
|
if section_scope is not UNSET:
|
|
cfg.set("Svc", "flood_scope", section_scope)
|
|
return cfg
|
|
|
|
|
|
def _bot(cfg):
|
|
bot = MagicMock()
|
|
bot.config = cfg
|
|
bot.logger = Mock()
|
|
bot.connected = True
|
|
bot.is_radio_zombie = False
|
|
bot.is_radio_offline = False
|
|
bot.meshcore = MagicMock()
|
|
bot.meshcore.self_info = {"name": "ScopeBot"}
|
|
bot.channel_manager = MagicMock()
|
|
bot.channel_manager.get_channel_number = Mock(return_value=1)
|
|
bot.transmission_tracker = None
|
|
return bot
|
|
|
|
|
|
def _cm(bot):
|
|
cm = object.__new__(CommandManager)
|
|
cm.bot = bot
|
|
cm.logger = bot.logger
|
|
cm._check_rate_limits = AsyncMock(return_value=(True, None))
|
|
cm._is_no_event_received = Mock(return_value=False)
|
|
cm._handle_send_result = Mock(return_value=True)
|
|
bot.command_manager = cm
|
|
return cm
|
|
|
|
|
|
def _radio_scopes(cm, bot, scope):
|
|
"""Scopes handed to set_flood_scope by one real send_channel_message call."""
|
|
calls: list[str] = []
|
|
|
|
async def _set(value):
|
|
calls.append(value)
|
|
return MagicMock(type=EventType.OK)
|
|
|
|
bot.meshcore.commands.set_flood_scope = AsyncMock(side_effect=_set)
|
|
bot.meshcore.commands.send_chan_msg = AsyncMock(
|
|
return_value=MagicMock(type=EventType.OK, payload={})
|
|
)
|
|
asyncio.run(cm.send_channel_message("general", "hi", scope=scope))
|
|
return calls
|
|
|
|
|
|
def _safe(fn):
|
|
try:
|
|
return fn()
|
|
except Exception as exc: # noqa: BLE001 - the exception type is the behavior
|
|
return f"<raises {type(exc).__name__}>"
|
|
|
|
|
|
def test_scope_resolution_matrix():
|
|
results = {}
|
|
for override, chan, section in itertools.product(OVERRIDES, CHANNEL_SCOPES, SECTION_SCOPES):
|
|
cfg = _config(override, chan, section)
|
|
bot = _bot(cfg)
|
|
cm = _cm(bot)
|
|
sched = object.__new__(MessageScheduler)
|
|
sched.bot = bot
|
|
sched.logger = bot.logger
|
|
svc = object.__new__(_Svc)
|
|
svc.bot = bot
|
|
cfg_key = f"override={override!r} channel={chan!r} section={section!r}"
|
|
row = {
|
|
"service_scope": _safe(svc.get_mesh_flood_scope),
|
|
}
|
|
for reply in REPLY:
|
|
msg = MeshMessage(content="x", channel="general", is_dm=False, reply_scope=reply)
|
|
row[f"message.effective(reply={reply!r})"] = _safe(
|
|
lambda msg=msg: msg.effective_outgoing_flood_scope(bot)
|
|
)
|
|
row[f"resolve(msg reply={reply!r}, section)"] = _safe(
|
|
lambda msg=msg: cm.resolve_channel_send_scope(
|
|
message=msg, config_section="Svc", channel="general"
|
|
)
|
|
)
|
|
dm = MeshMessage(content="x", channel=None, is_dm=True, reply_scope="#r")
|
|
row["message.effective(dm)"] = _safe(lambda: dm.effective_outgoing_flood_scope(bot))
|
|
for explicit in EXPLICIT:
|
|
key = f"explicit={explicit!r}"
|
|
row[f"resolve({key})"] = _safe(
|
|
lambda e=explicit: cm.resolve_channel_send_scope(scope=e, channel="general")
|
|
)
|
|
row[f"effective({key})"] = _safe(
|
|
lambda e=explicit: cm.effective_channel_send_scope(channel="general", scope=e)
|
|
)
|
|
row[f"budget({key})"] = _safe(
|
|
lambda e=explicit: cm.channel_body_budget(channel="general", scope=e)
|
|
)
|
|
sched_scope = _safe(lambda e=explicit: sched._effective_send_scope("general", e))
|
|
row[f"scheduler.scope({key})"] = sched_scope
|
|
row[f"scheduler.budget({key})"] = _safe(
|
|
lambda s=sched_scope: sched._channel_body_budget(s)
|
|
)
|
|
row[f"radio({key})"] = _safe(lambda e=explicit: _radio_scopes(cm, bot, e))
|
|
# A channel with no per-channel entry, to separate the override path.
|
|
row["effective(other channel)"] = _safe(
|
|
lambda: cm.effective_channel_send_scope(channel="other", scope=None)
|
|
)
|
|
results[cfg_key] = row
|
|
assert_golden("scope_matrix", results)
|
|
|
|
|
|
def test_global_marker_predicates_agree():
|
|
from modules.flood_scope import is_global_marker
|
|
|
|
for value in ["", "*", "0", "None", "none", "NONE", "#west", "west", " ", "nOnE"]:
|
|
assert MeshMessage.is_global_flood_scope(value) == is_global_marker(value), value
|