diff --git a/pytests/helpers.py b/pytests/helpers.py index 749c526..f78343e 100644 --- a/pytests/helpers.py +++ b/pytests/helpers.py @@ -22,6 +22,10 @@ def freeze_display_info( method: Type[BrightnessMethod], patch_get_display_info ) -> List[dict]: + ''' + Calls `get_display_info`, stores the result, and mocks it to return the same + result every time it's called + ''' displays = method.get_display_info() mocker.patch.object(method, 'get_display_info', Mock(return_value=displays), spec=True) return displays @@ -30,12 +34,12 @@ def freeze_display_info( @pytest.fixture def patch_get_brightness(self, mocker: MockerFixture, patch_get_display_info): '''Applies patches to get `get_brightness` working''' - ... + raise NotImplementedError() @pytest.fixture def patch_set_brightness(self, mocker: MockerFixture, patch_get_display_info): '''Applies patches to get `set_brightness` working''' - ... + raise NotImplementedError() @pytest.fixture def method(self) -> Type[BrightnessMethod]: @@ -97,6 +101,7 @@ def brightness(self, method: Type[BrightnessMethod]) -> List[int]: return method.get_brightness() class TestDisplayKwarg(ABC): + # TODO: most of these tests are generic enough they could be implemented in a parent class def test_with(self): '''Test what happens when display kwarg is given. Only one display should be polled''' raise NotImplementedError() @@ -105,20 +110,20 @@ def test_without(self): '''Test what happens when no display kwarg is given. All displays should be polled''' raise NotImplementedError() + def test_only_returns_brightness_of_requested_display(self, method: Type[BrightnessMethod]): + for i in range(len(method.get_display_info())): + brightness = method.get_brightness(display=i) + assert isinstance(brightness, list) + assert len(brightness) == 1 + assert isinstance(brightness[0], int) + assert 0 <= brightness[0] <= 100 + def test_returns_list_of_integers(self, method: Type[BrightnessMethod], brightness): assert isinstance(brightness, list) assert all(isinstance(i, int) for i in brightness) assert all(0 <= i <= 100 for i in brightness) assert len(brightness) == len(method.get_display_info()) - def test_only_returns_brightness_of_requested_display(self, method: Type[BrightnessMethod]): - for i in range(len(method.get_display_info())): - brightness = method.get_brightness(display=i) - assert isinstance(brightness, list) - assert len(brightness) == 1 - assert isinstance(brightness[0], int) - assert 0 <= brightness[0] <= 100 - class TestSetBrightness(ABC): @pytest.fixture(autouse=True) def patch(self, patch_set_brightness): diff --git a/pytests/mocks/linux_mock.py b/pytests/mocks/linux_mock.py new file mode 100644 index 0000000..ebc0a08 --- /dev/null +++ b/pytests/mocks/linux_mock.py @@ -0,0 +1,69 @@ +import re +from typing import Dict, Optional, Tuple +from screen_brightness_control.linux import I2C + +def fake_edid(mfg_id: str, name: str, serial: str) -> str: + def descriptor(string: str) -> str: + return string.encode('utf-8').hex() + ('20' * (13 - len(string))) + + mfg_ords = [ord(i) - 64 for i in mfg_id] + mfg = mfg_ords[0] << 10 | mfg_ords[1] << 5 | mfg_ords[2] + + return ''.join(( + '00ffffffffffff00', # header + f'{mfg:04x}', # 'DEL' mfg id + '00' * 44, # product id -> edid timings + '00' * 18, # empty descriptor block + f'000000fc00{descriptor(name)}', # name descriptor + f'000000ff00{descriptor(serial)}', # serial descriptor + '00' * 18, # empty descriptor + '00' # extension flag + '00' # checksum - TODO: make this actually work + )) + + +class MockI2C: + class MockI2CDevice: + _fake_devices = ( + ('DEL', 'Dell ABC123', 'serial123'), + ('BNQ', 'BenQ DEF456', 'serial456') + ) + + def __init__(self, path: str, addr: int): + match = re.match(r'/dev/i2c-(\d+)', path) + assert match, 'device path does not match expected format' + self._index = int(match.group(1)) + assert addr in (I2C.HOST_ADDR_R, I2C.DDCCI_ADDR) + self._path = path + self._addr = addr + + def read(self, length: int) -> bytes: + if self._addr == I2C.HOST_ADDR_R: + edid = fake_edid(*self._fake_devices[self._index]) + assert len(edid) == 256, '128 bytes is 256 string chars' + return bytes.fromhex(('00' * 128) + edid + ('00' * 128)) + raise NotImplementedError() + + def write(self, data: bytes) -> int: + return len(data) + + class MockDDCInterface(MockI2CDevice): + def __init__(self, i2c_path: str): + super().__init__(i2c_path, I2C.DDCCI_ADDR) + + self._vcp_state: Dict[int, int] = {} + + def write(self, *args) -> int: + raise NotImplementedError() + + def read(self, amount: int) -> bytes: + raise NotImplementedError() + + def setvcp(self, vcp_code: int, value: int) -> int: + self._vcp_state[vcp_code] = value + return 0 + + def getvcp(self, vcp_code: int) -> Tuple[int, int]: + assert vcp_code == 0x10, 'should only be getting the brightness' + # current and max brightness + return self._vcp_state.get(vcp_code, 100), 100 diff --git a/pytests/test_linux.py b/pytests/test_linux.py index c15fa0c..2550d1f 100644 --- a/pytests/test_linux.py +++ b/pytests/test_linux.py @@ -1,11 +1,15 @@ +import glob import os -from typing import Type -from unittest.mock import MagicMock, Mock +import re +from typing import Dict, Optional, Type +from unittest.mock import Mock import pytest +from .mocks.linux_mock import MockI2C from pytest_mock import MockerFixture import screen_brightness_control as sbc +from screen_brightness_control import linux from screen_brightness_control.helpers import BrightnessMethod from .helpers import BrightnessMethodTest @@ -89,3 +93,61 @@ def test_without(self, mocker: MockerFixture, method: Type[BrightnessMethod], fr for index, display in enumerate(freeze_display_info): mock.assert_any_call(os.path.join(display['path'], 'brightness'), 'w') assert write.call_args_list[index][0][0] == '100' + + +class TestI2C(BrightnessMethodTest): + @pytest.fixture + def patch_get_display_info(self, mocker: MockerFixture): + def path_exists(path: str): + return re.match(r'/dev/i2c-\d+', path) is not None + + mocker.patch.object(glob, 'glob', Mock(return_value=['/dev/i2c-0', '/dev/i2c-1']), spec=True) + mocker.patch.object(os.path, 'exists', Mock(side_effect=path_exists), spec=True) + mocker.patch.object(linux.I2C, 'I2CDevice', MockI2C.MockI2CDevice, spec=True) + + @pytest.fixture + def patch_get_brightness(self, mocker: MockerFixture, patch_get_display_info): + mocker.patch.object(linux.I2C, 'DDCInterface', MockI2C.MockDDCInterface, spec=True) + + @pytest.fixture + def method(self): + return linux.I2C + + class TestGetDisplayInfo(BrightnessMethodTest.TestGetDisplayInfo): + def test_returned_dicts_contain_required_keys(self, method: type[BrightnessMethod]): + return super().test_returned_dicts_contain_required_keys(method, {'i2c_bus': str}) + + def test_display_filtering(self, mocker: MockerFixture, original_os_module, method): + return super().test_display_filtering(mocker, original_os_module, method, {'include': ['i2c_bus']}) + + class TestGetBrightness(BrightnessMethodTest.TestGetBrightness): + class TestDisplayKwarg(BrightnessMethodTest.TestGetBrightness.TestDisplayKwarg): + def test_with(self, mocker: MockerFixture, method: Type[BrightnessMethod], freeze_display_info): + spy = mocker.spy(method, 'DDCInterface') + for index, display in enumerate(freeze_display_info): + method.get_brightness(display=index) + spy.assert_called_once_with(display['i2c_bus']) + spy.reset_mock() + + def test_without(self, mocker: MockerFixture, method: Type[BrightnessMethod], freeze_display_info): + spy = mocker.spy(method, 'DDCInterface') + method.get_brightness() + paths = [device['i2c_bus'] for device in freeze_display_info] + called_devices = [i[0][0] for i in spy.call_args_list] + assert paths == called_devices + + class TestSetBrightness(BrightnessMethodTest.TestSetBrightness): + class TestDisplayKwarg(BrightnessMethodTest.TestSetBrightness.TestDisplayKwarg): + def test_with(self, mocker: MockerFixture, method: Type[BrightnessMethod], freeze_display_info): + spy = mocker.spy(method, 'DDCInterface') + for index, display in enumerate(freeze_display_info): + method.set_brightness(100, display=index) + spy.assert_called_once_with(display['i2c_bus']) + spy.reset_mock() + + def test_without(self, mocker: MockerFixture, method: Type[BrightnessMethod], freeze_display_info): + spy = mocker.spy(method, 'DDCInterface') + method.set_brightness(100) + paths = [device['i2c_bus'] for device in freeze_display_info] + called_devices = [i[0][0] for i in spy.call_args_list] + assert paths == called_devices