"""Port selection and error explanation, using a fake reader library.""" import types import pytest import rdm_card_reader.reader as reader_module from rdm_card_reader.card import CANDIDATE_PORTS from rdm_card_reader.reader import explain_error, list_ports, open_reader class FakeBaseReader: """Stands in for rdm6300.BaseReader; records the port it was built with.""" fail_on = () def __init__(self, port, heartbeat_interval=0.5): if port in type(self).fail_on: raise OSError("could not open port {}".format(port)) self.port = port self.heartbeat_interval = heartbeat_interval self.started = False self.closed = False def start(self): self.started = True def close(self): self.closed = True @pytest.fixture def fake_library(monkeypatch): module = types.SimpleNamespace(BaseReader=FakeBaseReader) monkeypatch.setattr(reader_module, "rdm6300", module) return module @pytest.fixture(autouse=True) def restore_library(): original = reader_module.rdm6300 yield reader_module.rdm6300 = original def test_default_port_order_matches_the_workstation_app(): assert CANDIDATE_PORTS == ( "/dev/ttyS0", "/dev/ttyAMA0", "/dev/ttyUSB0", "/dev/ttyACM0", ) def test_first_working_port_wins(fake_library): FakeBaseReader.fail_on = ("/dev/ttyS0", "/dev/ttyAMA0") reader = open_reader(lambda event: None, ports=["/dev/ttyS0", "/dev/ttyAMA0", "/dev/ttyUSB0"]) assert reader is not None assert reader.port == "/dev/ttyUSB0" def test_returns_none_when_no_port_works(fake_library): FakeBaseReader.fail_on = ("/dev/ttyS0", "/dev/ttyAMA0") assert open_reader(lambda event: None, ports=["/dev/ttyS0", "/dev/ttyAMA0"]) is None def test_failures_are_reported_with_the_port(fake_library): FakeBaseReader.fail_on = ("/dev/ttyS0",) seen = [] open_reader(lambda event: None, ports=["/dev/ttyS0"], on_error=lambda port, message: seen.append((port, message))) assert seen and seen[0][0] == "/dev/ttyS0" def test_heartbeat_interval_is_passed_through(fake_library): FakeBaseReader.fail_on = () reader = open_reader(lambda event: None, ports=["/dev/ttyS0"], heartbeat_interval=1.5) assert reader.heartbeat_interval == 1.5 def test_heartbeat_none_disables_removal_detection(fake_library): FakeBaseReader.fail_on = () reader = open_reader(lambda event: None, ports=["/dev/ttyS0"], heartbeat_interval=None) assert reader.heartbeat_interval is None def test_events_are_forwarded_to_the_callback(fake_library): FakeBaseReader.fail_on = () received = [] reader = open_reader(received.append, ports=["/dev/ttyS0"]) class Card: value = 7 checksum = 1 type = 15 is_valid = True reader.card_inserted(Card()) assert len(received) == 1 assert received[0].value == 7 assert received[0].event == "card_inserted" def test_card_removed_with_none_is_ignored(fake_library): FakeBaseReader.fail_on = () received = [] reader = open_reader(received.append, ports=["/dev/ttyS0"]) reader.card_removed(None) assert received == [] def test_invalid_card_is_forwarded(fake_library): FakeBaseReader.fail_on = () received = [] reader = open_reader(received.append, ports=["/dev/ttyS0"]) class Card: value = 1 checksum = 2 type = 15 is_valid = False reader.invalid_card(Card()) assert received[0].event == "invalid_card" def test_missing_library_raises(monkeypatch): monkeypatch.setattr(reader_module, "rdm6300", None) with pytest.raises(reader_module.ReaderUnavailable): open_reader(lambda event: None) @pytest.mark.parametrize( "message,expected_fragment", [ ("[Errno 13] Permission denied: '/dev/ttyS0'", "dialout"), ("could not open port /dev/nope", "device not found"), ("something else", "something else"), ], ) def test_explain_error(message, expected_fragment): assert expected_fragment in explain_error("/dev/ttyS0", message) def test_list_ports_shape(): info = list_ports() assert set(info) == {"candidates", "detected"} assert [port for port, _ in info["candidates"]] == list(CANDIDATE_PORTS)