128 lines
4.5 KiB
Python
128 lines
4.5 KiB
Python
import unittest
|
|
from collections import Counter
|
|
|
|
from ccu_emulator.controller import CcuConfig, CcuEmulator
|
|
from ccu_emulator.frames import ACTIVE_SEED_COMMAND0, NEUTRAL_ACK_FRAME, build_frame, frame_checksum_ok
|
|
from ccu_emulator.iris_mblack_link import IrisMblackLinkModule
|
|
from ccu_emulator.policy import AckPolicy
|
|
from ccu_emulator.refresh import PeriodicRefresh
|
|
from ccu_emulator.serial_link import RxFrame
|
|
|
|
|
|
class CcuEmulatorFrameTests(unittest.TestCase):
|
|
def test_build_frame_adds_checksum(self):
|
|
self.assertEqual(build_frame(0x00, 0x0000, 0x8080), ACTIVE_SEED_COMMAND0)
|
|
self.assertTrue(frame_checksum_ok(build_frame(0x06, 0x0015, 0x0001)))
|
|
|
|
def test_ack_policy_acknowledges_reports_with_neutral_ack(self):
|
|
decision = AckPolicy().decide(bytes.fromhex("02000200005A"), "connect_ok_path_response_candidate")
|
|
self.assertTrue(decision.should_ack)
|
|
self.assertEqual(decision.frame, NEUTRAL_ACK_FRAME)
|
|
|
|
def test_ack_policy_skips_table_readback(self):
|
|
decision = AckPolicy().decide(bytes.fromhex("04000080805E"), "table_readback_candidate")
|
|
self.assertFalse(decision.should_ack)
|
|
|
|
|
|
class PeriodicRefreshTests(unittest.TestCase):
|
|
def test_periodic_refresh_returns_due_frames(self):
|
|
refresh = PeriodicRefresh(frames=[ACTIVE_SEED_COMMAND0], interval=0.5)
|
|
refresh.start(now=10.0)
|
|
self.assertEqual(refresh.due_frames(now=10.4), [])
|
|
self.assertEqual(refresh.due_frames(now=10.5), [ACTIVE_SEED_COMMAND0])
|
|
self.assertEqual(refresh.due_frames(now=10.6), [])
|
|
|
|
|
|
class IrisMblackLinkModuleTests(unittest.TestCase):
|
|
def test_active_report_gets_selector_ack_and_mirror(self):
|
|
module = IrisMblackLinkModule(mirror_delay=0.012)
|
|
|
|
decision = module.on_rx(bytes.fromhex("000013400009"), "known_iris_mblack_link_active_report_candidate")
|
|
|
|
self.assertIsNotNone(decision)
|
|
assert decision is not None
|
|
self.assertTrue(decision.suppress_default_ack)
|
|
self.assertEqual(
|
|
[tx.frame for tx in decision.tx],
|
|
[bytes.fromhex("05001300004C"), bytes.fromhex("000013400009")],
|
|
)
|
|
self.assertEqual(decision.tx[1].delay, 0.012)
|
|
self.assertIn("active", decision.reason)
|
|
|
|
def test_clear_report_gets_selector_ack_and_mirror(self):
|
|
module = IrisMblackLinkModule()
|
|
|
|
decision = module.on_rx(bytes.fromhex("02001300004B"), "queued_iris_mblack_link_clear_report_candidate")
|
|
|
|
self.assertIsNotNone(decision)
|
|
assert decision is not None
|
|
self.assertEqual(
|
|
[tx.frame for tx in decision.tx],
|
|
[bytes.fromhex("05001300004C"), bytes.fromhex("000013000049")],
|
|
)
|
|
self.assertIn("clear", decision.reason)
|
|
|
|
def test_non_0013_report_is_ignored(self):
|
|
module = IrisMblackLinkModule()
|
|
|
|
self.assertIsNone(module.on_rx(bytes.fromhex("0000158000CF"), "known_call_button_active_report"))
|
|
|
|
|
|
class CcuEmulatorModuleIntegrationTests(unittest.TestCase):
|
|
def test_module_response_suppresses_generic_neutral_ack(self):
|
|
link = FakeLink(RxFrame(bytes.fromhex("000013400009"), "known_iris_mblack_link_active_report_candidate"))
|
|
logger = FakeLogger()
|
|
emulator = CcuEmulator(
|
|
link,
|
|
logger,
|
|
config=CcuConfig(seed_frames=(), ready_heartbeats=0),
|
|
modules=(IrisMblackLinkModule(mirror_delay=0.0),),
|
|
)
|
|
|
|
emulator._service_rx()
|
|
|
|
self.assertEqual(
|
|
[frame for frame, _label in link.sent],
|
|
[bytes.fromhex("05001300004C"), bytes.fromhex("000013400009")],
|
|
)
|
|
self.assertEqual(emulator.stats.ack_frames, 0)
|
|
self.assertEqual(emulator.stats.module_frames, 2)
|
|
self.assertEqual(emulator.stats.tx_frames, 2)
|
|
|
|
|
|
class FakeDetector:
|
|
def __init__(self) -> None:
|
|
self.labels = Counter()
|
|
self.resync_events = 0
|
|
self.dropped_bytes = 0
|
|
|
|
|
|
class FakeLink:
|
|
def __init__(self, *items: RxFrame) -> None:
|
|
self.items = list(items)
|
|
self.sent: list[tuple[bytes, str]] = []
|
|
self.detector = FakeDetector()
|
|
|
|
def read_available(self) -> list[RxFrame]:
|
|
if not self.items:
|
|
return []
|
|
return [self.items.pop(0)]
|
|
|
|
def send(self, frame: bytes, label: str) -> None:
|
|
self.sent.append((frame, label))
|
|
|
|
|
|
class FakeLogger:
|
|
def __init__(self) -> None:
|
|
self.lines: list[str] = []
|
|
|
|
def event(self, text: str) -> None:
|
|
self.lines.append(text)
|
|
|
|
def emit(self, line: str = "") -> None:
|
|
self.lines.append(line)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|