From ef6b7dec2c0ff288058ac3d4f2aa12967be149be Mon Sep 17 00:00:00 2001 From: MattHag <16444067+MattHag@users.noreply.github.com> Date: Sat, 28 Sep 2024 18:45:17 +0200 Subject: [PATCH] receiver: Remove hard dependency on base With this test all receiver tests are macOS compatible again. The low level interface supports passing a fake API for unit tests. --- lib/logitech_receiver/receiver.py | 22 +++--- lib/solaar/cli/__init__.py | 4 +- lib/solaar/listener.py | 2 +- tests/logitech_receiver/test_receiver.py | 87 +++++++++++++----------- 4 files changed, 60 insertions(+), 55 deletions(-) diff --git a/lib/logitech_receiver/receiver.py b/lib/logitech_receiver/receiver.py index f69a9357..5eff27b7 100644 --- a/lib/logitech_receiver/receiver.py +++ b/lib/logitech_receiver/receiver.py @@ -23,12 +23,10 @@ from dataclasses import dataclass from typing import Callable from typing import Optional from typing import Protocol -from typing import cast from solaar.i18n import _ from solaar.i18n import ngettext -from . import base from . import exceptions from . import hidpp10 from . import hidpp10_constants @@ -60,9 +58,6 @@ class LowLevelInterface(Protocol): ... -low_level_interface = cast(LowLevelInterface, base) - - @dataclass class Pairing: """Information about the current or most recent pairing""" @@ -91,6 +86,7 @@ class Receiver: def __init__( self, + low_level: LowLevelInterface, receiver_kind, product_info, handle, @@ -99,6 +95,7 @@ class Receiver: setting_callback=None, ): assert handle + self.low_level = low_level self.isDevice = False # some devices act as receiver so we need a property to distinguish them self.handle = handle self.path = path @@ -137,7 +134,7 @@ class Receiver: if d: d.close() self._devices.clear() - return handle and base.close(handle) + return handle and self.low_level.close(handle) def __del__(self): self.close() @@ -257,7 +254,7 @@ class Receiver: logger.warning("mismatch on device kind %s %s", info["kind"], nkind) else: online = True - dev = Device(low_level_interface, self, number, online, pairing_info=info, setting_callback=self.setting_callback) + dev = Device(self.low_level, self, number, online, pairing_info=info, setting_callback=self.setting_callback) if logger.isEnabledFor(logging.INFO): logger.info("%s: found new device %d (%s)", self, number, dev.wpid) self._devices[number] = dev @@ -282,7 +279,7 @@ class Receiver: def request(self, request_id, *params): if bool(self): - return base.request(self.handle, 0xFF, request_id, *params) + return self.low_level.request(self.handle, 0xFF, request_id, *params) def reset_pairing(self): self.pairing = Pairing() @@ -481,7 +478,7 @@ class Ex100Receiver(Receiver): def device_pairing_information(self, number: int) -> dict: # extract WPID from udev path - wpid = base.find_paired_node_wpid(self.path, number) + wpid = self.low_level.find_paired_node_wpid(self.path, number) if not wpid: logger.error("Unable to get wpid from udev for device %d of %s", number, self) raise exceptions.NoSuchDevice(number=number, receiver=self, error="Not present 27Mhz device") @@ -517,22 +514,23 @@ receiver_class_mapping = { class ReceiverFactory: @staticmethod - def create_receiver(device_info, setting_callback=None) -> Optional[Receiver]: + def create_receiver(low_level: LowLevelInterface, device_info, setting_callback=None) -> Optional[Receiver]: """Opens a Logitech Receiver found attached to the machine, by Linux device path.""" try: - handle = base.open_path(device_info.path) + handle = low_level.open_path(device_info.path) if handle: usb_id = device_info.product_id if isinstance(usb_id, str): usb_id = int(usb_id, 16) try: - product_info = base.product_information(usb_id) + product_info = low_level.product_information(usb_id) except ValueError: product_info = {} kind = product_info.get("receiver_kind", "unknown") rclass = receiver_class_mapping.get(kind, Receiver) return rclass( + low_level, kind, product_info, handle, diff --git a/lib/solaar/cli/__init__.py b/lib/solaar/cli/__init__.py index 927b871c..07244f6f 100644 --- a/lib/solaar/cli/__init__.py +++ b/lib/solaar/cli/__init__.py @@ -106,7 +106,7 @@ def _receivers(dev_path=None): if dev_path is not None and dev_path != dev_info.path: continue try: - r = receiver.ReceiverFactory.create_receiver(dev_info) + r = receiver.ReceiverFactory.create_receiver(base, dev_info) if logger.isEnabledFor(logging.DEBUG): logger.debug("[%s] => %s", dev_info.path, r) if r: @@ -124,7 +124,7 @@ def _receivers_and_devices(dev_path=None): if dev_info.isDevice: d = device.DeviceFactory.create_device(base, dev_info) else: - d = receiver.ReceiverFactory.create_receiver(dev_info) + d = receiver.ReceiverFactory.create_receiver(base, dev_info) if logger.isEnabledFor(logging.DEBUG): logger.debug("[%s] => %s", dev_info.path, d) diff --git a/lib/solaar/listener.py b/lib/solaar/listener.py index ef76d8a8..7085bed6 100644 --- a/lib/solaar/listener.py +++ b/lib/solaar/listener.py @@ -255,7 +255,7 @@ def _start(device_info): assert _status_callback and _setting_callback isDevice = device_info.isDevice if not isDevice: - receiver_ = logitech_receiver.receiver.ReceiverFactory.create_receiver(device_info, _setting_callback) + receiver_ = logitech_receiver.receiver.ReceiverFactory.create_receiver(base, device_info, _setting_callback) else: receiver_ = logitech_receiver.device.DeviceFactory.create_device(base, device_info, _setting_callback) if receiver_: diff --git a/tests/logitech_receiver/test_receiver.py b/tests/logitech_receiver/test_receiver.py index 014e20fd..947ea046 100644 --- a/tests/logitech_receiver/test_receiver.py +++ b/tests/logitech_receiver/test_receiver.py @@ -1,11 +1,10 @@ -import platform - from dataclasses import dataclass from functools import partial from unittest import mock import pytest +from logitech_receiver import base from logitech_receiver import common from logitech_receiver import exceptions from logitech_receiver import receiver @@ -13,6 +12,31 @@ from logitech_receiver import receiver from . import fake_hidpp +class LowLevelInterfaceFake: + def __init__(self, responses=None): + self.responses = responses + + def open_path(self, path): + return fake_hidpp.open_path(path) + + def product_information(self, usb_id: int) -> dict: + return base.product_information(usb_id) + + def find_paired_node(self, receiver_path: str, index: int, timeout: int): + return None + + def request(self, response, *args, **kwargs): + func = partial(fake_hidpp.request, self.responses) + return func(response, *args, **kwargs) + + def ping(self, response, *args, **kwargs): + func = partial(fake_hidpp.ping, self.responses) + return func(response, *args, **kwargs) + + def close(self, *args, **kwargs): + pass + + @pytest.mark.parametrize( "index, expected_kind", [ @@ -47,13 +71,6 @@ def mock_request(): yield mock_request -@pytest.fixture -def mock_base(): - with mock.patch("logitech_receiver.base.open_path", return_value=None) as mock_open_path: - with mock.patch("logitech_receiver.base.request", return_value=None) as mock_request: - yield mock_open_path, mock_request - - responses_unifying = [ fake_hidpp.Response("000000", 0x8003, "FF"), fake_hidpp.Response("000300", 0x8102), @@ -99,37 +116,34 @@ mouse_info = { c534_info = {"kind": common.NamedInt(0, "unknown"), "polling": "", "power_switch": "(unknown)", "serial": None, "wpid": "45AB"} -@pytest.mark.skipif(platform.system() == "Darwin", reason="Fails on macOS") @pytest.mark.parametrize( "device_info, responses, handle, serial, max_devices, ", [ - (DeviceInfo(None), [], False, None, None), - (DeviceInfo(11), [], None, None, None), - (DeviceInfo("11"), responses_unifying, 0x11, "16CC9CB4", 6), - (DeviceInfo("12", product_id=0xC534), responses_c534, 0x12, "16CC9CB4", 2), - (DeviceInfo("12", product_id=0xC539), responses_c534, 0x12, "16CC9CB4", 2), - (DeviceInfo("13"), responses_unusual, 0x13, "26CC9CB4", 1), - (DeviceInfo("14"), responses_lacking, 0x14, None, 1), + (DeviceInfo(path=None), [], False, None, None), + (DeviceInfo(path=11), [], None, None, None), + (DeviceInfo(path="11"), responses_unifying, 0x11, "16CC9CB4", 6), + (DeviceInfo(path="12", product_id=0xC534), responses_c534, 0x12, "16CC9CB4", 2), + (DeviceInfo(path="12", product_id=0xC539), responses_c534, 0x12, "16CC9CB4", 2), + (DeviceInfo(path="13"), responses_unusual, 0x13, "26CC9CB4", 1), + (DeviceInfo(path="14"), responses_lacking, 0x14, None, 1), ], ) -def test_receiver_factory_create_receiver(device_info, responses, handle, serial, max_devices, mock_base): - mock_base[0].side_effect = fake_hidpp.open_path - mock_base[1].side_effect = partial(fake_hidpp.request, responses) +def test_receiver_factory_create_receiver(device_info, responses, handle, serial, max_devices): + mock_low_level = LowLevelInterfaceFake(responses) if handle is False: with pytest.raises(Exception): # noqa: B017 - receiver.ReceiverFactory.create_receiver(device_info, lambda x: x) + receiver.ReceiverFactory.create_receiver(mock_low_level, device_info, lambda x: x) elif handle is None: - r = receiver.ReceiverFactory.create_receiver(device_info, lambda x: x) + r = receiver.ReceiverFactory.create_receiver(mock_low_level, device_info, lambda x: x) assert r is None else: - r = receiver.ReceiverFactory.create_receiver(device_info, lambda x: x) + r = receiver.ReceiverFactory.create_receiver(mock_low_level, device_info, lambda x: x) assert r.handle == handle assert r.serial == serial assert r.max_devices == max_devices -@pytest.mark.skipif(platform.system() == "Darwin", reason="Fails on macOS") @pytest.mark.parametrize( "device_info, responses, firmware, codename, remaining_pairings, pairing_info, count", [ @@ -138,13 +152,10 @@ def test_receiver_factory_create_receiver(device_info, responses, handle, serial (DeviceInfo("13", product_id=0xCCCC), responses_unusual, None, None, -1, c534_info, 3), ], ) -def test_receiver_factory_props( - device_info, responses, firmware, codename, remaining_pairings, pairing_info, count, mock_base -): - mock_base[0].side_effect = fake_hidpp.open_path - mock_base[1].side_effect = partial(fake_hidpp.request, responses) +def test_receiver_factory_props(device_info, responses, firmware, codename, remaining_pairings, pairing_info, count): + mock_low_level = LowLevelInterfaceFake(responses) - r = receiver.ReceiverFactory.create_receiver(device_info, lambda x: x) + r = receiver.ReceiverFactory.create_receiver(mock_low_level, device_info, lambda x: x) assert len(r.firmware) == firmware if firmware is not None else firmware is None assert r.device_codename(2) == codename @@ -153,7 +164,6 @@ def test_receiver_factory_props( assert r.count() == count -@pytest.mark.skipif(platform.system() == "Darwin", reason="Fails on macOS") @pytest.mark.parametrize( "device_info, responses, status_str, strng", [ @@ -162,17 +172,15 @@ def test_receiver_factory_props( (DeviceInfo("13", product_id=0xCCCC), responses_unusual, "No paired devices.", ""), ], ) -def test_receiver_factory_string(device_info, responses, status_str, strng, mock_base): - mock_base[0].side_effect = fake_hidpp.open_path - mock_base[1].side_effect = partial(fake_hidpp.request, responses) +def test_receiver_factory_string(device_info, responses, status_str, strng): + mock_low_level = LowLevelInterfaceFake(responses) - r = receiver.ReceiverFactory.create_receiver(device_info, lambda x: x) + r = receiver.ReceiverFactory.create_receiver(mock_low_level, device_info, lambda x: x) assert r.status_string() == status_str assert str(r) == strng -@pytest.mark.skipif(platform.system() == "Darwin", reason="Fails on macOS") @pytest.mark.parametrize( "device_info, responses", [ @@ -180,11 +188,10 @@ def test_receiver_factory_string(device_info, responses, status_str, strng, mock (DeviceInfo("14", product_id="C534"), responses_lacking), ], ) -def test_receiver_factory_nodevice(device_info, responses, mock_base): - mock_base[0].side_effect = fake_hidpp.open_path - mock_base[1].side_effect = partial(fake_hidpp.request, responses) +def test_receiver_factory_no_device(device_info, responses): + mock_low_level = LowLevelInterfaceFake(responses) - r = receiver.ReceiverFactory.create_receiver(device_info, lambda x: x) + r = receiver.ReceiverFactory.create_receiver(mock_low_level, device_info, lambda x: x) with pytest.raises(exceptions.NoSuchDevice): r.device_pairing_information(1)