import can import pytest from lingkong_motor_service.config import Settings from lingkong_motor_service.protocol import build_open_loop_frame from lingkong_motor_service.transport import ( CanTransportError, DisabledCanTransport, PythonCanTransport, ) class StubBus: def __init__(self) -> None: self.messages: list[can.Message] = [] self.timeouts: list[float | None] = [] self.shutdown_count = 0 self.received_messages: list[can.Message] = [] def send(self, message: can.Message, timeout: float | None = None) -> None: self.messages.append(message) self.timeouts.append(timeout) def shutdown(self) -> None: self.shutdown_count += 1 def recv(self, timeout: float | None = None) -> can.Message | None: del timeout if not self.received_messages: return None return self.received_messages.pop(0) def test_canalystii_configuration_and_standard_frame(monkeypatch: pytest.MonkeyPatch) -> None: captured: dict[str, object] = {} bus = StubBus() def open_bus(**kwargs: object) -> StubBus: captured.update(kwargs) return bus monkeypatch.setattr(can, "Bus", open_bus) transport = PythonCanTransport(Settings(hardware_enabled=True, can_channel=1, can_device=2)) try: transport.send(build_open_loop_frame({1: 400, 2: -400})) assert captured == { "interface": "canalystii", "channel": 1, "device": 2, "bitrate": 1_000_000, "rx_queue_size": 256, "receive_own_messages": False, } assert bus.timeouts == [None] assert len(bus.messages) == 1 assert bus.messages[0].arbitration_id == 0x280 assert bus.messages[0].is_extended_id is False assert bytes(bus.messages[0].data) == bytes.fromhex("90 01 70 FE 00 00 00 00") finally: transport.shutdown() def test_disabled_transport_fails_closed() -> None: with pytest.raises(CanTransportError, match="hardware access is disabled"): DisabledCanTransport().send(build_open_loop_frame({1: 0})) def test_canalystii_can_wait_for_the_hardware_tx_queue( monkeypatch: pytest.MonkeyPatch, ) -> None: bus = StubBus() monkeypatch.setattr(can, "Bus", lambda **kwargs: bus) transport = PythonCanTransport(Settings(hardware_enabled=True)) try: transport.send( build_open_loop_frame({}), tx_queue_timeout_s=0.1, ) assert bus.timeouts == [0.1] finally: transport.shutdown() def test_shutdown_is_idempotent_and_send_after_shutdown_fails( monkeypatch: pytest.MonkeyPatch, ) -> None: bus = StubBus() monkeypatch.setattr(can, "Bus", lambda **kwargs: bus) transport = PythonCanTransport(Settings(hardware_enabled=True)) transport.shutdown() transport.shutdown() assert transport.is_open is False assert bus.shutdown_count == 1 with pytest.raises(CanTransportError, match="CAN transport is closed"): transport.send(build_open_loop_frame({1: 0})) def test_canalystii_receives_standard_status_frame(monkeypatch: pytest.MonkeyPatch) -> None: bus = StubBus() bus.received_messages.append( can.Message( arbitration_id=0x141, is_extended_id=False, data=bytes.fromhex("9C 4F 64 00 0A 00 34 12"), ) ) monkeypatch.setattr(can, "Bus", lambda **kwargs: bus) transport = PythonCanTransport(Settings(hardware_enabled=True)) try: frame = transport.receive(timeout_s=0.01) assert frame is not None assert frame.arbitration_id == 0x141 assert frame.data == bytes.fromhex("9C 4F 64 00 0A 00 34 12") finally: transport.shutdown()